#region Using

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Net.Mail;
using System.Text;
using System.ComponentModel;

#endregion

namespace BlogEngine.Core
{
	/// <summary>
	/// Abstract Base class that implements all of the necessary functionality of a Publishable item.
	/// </summary>
	/// <typeparam name="T">type of object to instance</typeparam>
	[Serializable]
	public abstract class PublishableBase<T> : BlogBase<T>, IPublishable, IRateable, ITaggable, ICommentable where T : PublishableBase<T>, new()
	{
		/// <summary>
		/// Default constructor assigns all default values
		/// </summary>
		protected PublishableBase()
			: base()
		{
			_Comments = new List<Comment>();
			_Categories = new StateList<Category>();
			_Tags = new StateList<string>();
			_IsPublished = true;
			_IsCommentsEnabled = true;
		}

		#region properties

		private string _Content = string.Empty;
		/// <summary>
		/// Gets or sets the Content or the item.
		/// </summary>
		public string Content
		{
			get { return GetContentCore(_Content); }
			set
			{
                _Content = SetContentCore(_Content, value);
			}
		}

        protected virtual string SetContentCore(string current, string content)
        {
            if (current != content) MarkChanged("Content");
            return content;
        }
        protected virtual string GetContentCore(string current)
        {
            return current; 
        }

        protected void ClearContent()
        {
            _Content = string.Empty;
        }

		/// <summary>
		/// The absolute URI to the item.
		/// </summary>
		public Uri AbsoluteLink
		{
			get { return Utils.ConvertToAbsolute(AbsoluteLinkCore()); }
		}

		/// <summary>
		/// The absolute permanent link to the item.
		/// </summary>
		public Uri PermaLink
		{
			get { return new Uri(Utils.AbsoluteWebRoot.ToString() + PermaLinkPrefix() + ".aspx?id=" + Id.ToString()); }
		}

		/// <summary>
		/// A relative-to-the-site-root path to the item. Only for in-site use.
		/// </summary>
		public string RelativeLink
		{
			get { return RelativeLinkCore(); }
		}

		/// <summary>
		/// The trackback link to the published item.
		/// </summary>
		public Uri TrackbackLink
		{
			get { return new Uri(Utils.AbsoluteWebRoot.ToString() + "trackback.axd?id=" + Id.ToString()); }
		}

		private string _Author = string.Empty;

		/// <summary>
		/// Gets or sets the Author or the item.  By default the blog author name is returned
		/// </summary>
		public string Author
		{
			get { return string.IsNullOrEmpty(_Author) ? BlogSettings.Instance.AuthorName : _Author; }
			set
			{
				if (_Author != value) MarkChanged("Author");
				_Author = value;
			}
		}


		private StateList<Category> _Categories;
		/// <summary>
		/// An sorted collection of categories.
		/// </summary>
		public StateList<Category> Categories
		{
			get { return _Categories; }
		}

		private StateList<string> _Tags;
		/// <summary>
		/// An sorted collection of tags.
		/// </summary>
		public StateList<string> Tags
		{
			get { return _Tags; }
		}

		private float _Rating;
		/// <summary>
		/// Gets or sets the rating or the item.
		/// </summary>
		public float Rating
		{
			get { return _Rating; }
			set
			{
				if (_Rating != value) MarkChanged("Rating");
				_Rating = value;
			}
		}

		private int _Raters;
		/// <summary>
		/// Gets or sets the number of raters or the item.
		/// </summary>
		public int Raters
		{
			get { return _Raters; }
			set
			{
				if (_Raters != value) MarkChanged("Raters");
				_Raters = value;
			}
		}

		/// <summary>
		/// Gets whether or not the item is visible or not. An item is visible if it is published and
		/// the datecreated is in the past 
		/// </summary>
		public bool IsVisible
		{
			get
			{
				if (IsAuthenticated || IsPublished && DateCreated <= DateTime.Now.AddHours(BlogSettings.Instance.Timezone))
					return true;

				return false;
			}
		}

		private bool _IsPublished;

		/// <summary>
		/// Gets or sets the IsPublished or the item. Directly affects the IsVisible property
		/// </summary>
		public bool IsPublished
		{
			get { return _IsPublished; }
			set
			{
				if (_IsPublished != value) MarkChanged("IsPublished");
				_IsPublished = value;
			}
		}

		private StringCollection _NotificationEmails;
		/// <summary>
		/// Gets a collection of email addresses that is signed up for 
		/// comment notification on the specific item.
		/// </summary>
		public StringCollection NotificationEmails
		{
			get
			{
				if (_NotificationEmails == null)
					_NotificationEmails = new StringCollection();

				return _NotificationEmails;
			}
		}

		/// <summary>
		/// Gets if the item has been changed.
		/// </summary>
		public override bool IsChanged
		{
			get
			{
				if (base.IsChanged)
					return true;

				if (Tags.IsChanged) //TODO (IsCategoriesChanged || Tags.IsChanged) <-- this never worked but probably should
					return true;

				return false;
			}
		}

		private bool _IsCommentsEnabled;
		/// <summary>
		/// Gets or sets the EnableComments of the object.
		/// </summary>
		public bool IsCommentsEnabled
		{
			get { return _IsCommentsEnabled; }
			set
			{
				if (_IsCommentsEnabled != value) MarkChanged("IsCommentsEnabled");
				_IsCommentsEnabled = value;
			}
		}

		private readonly List<Comment> _Comments;
		/// <summary>
		/// A collection of Approved comments for the item, sorted by date.
		/// </summary>
		public List<Comment> ApprovedComments
		{
			get
			{
				return _Comments.FindAll(delegate(Comment obj) { return obj.IsApproved; });
			}
		}

		/// <summary>
		/// A collection of comments waiting for approval for the item, sorted by date.
		/// </summary>
		public List<Comment> NotApprovedComments
		{
			get
			{
				return _Comments.FindAll(delegate(Comment obj) { return !obj.IsApproved; });
			}
		}

		/// <summary>
		/// Collection of all Comments for the item whether they are approved or not
		/// </summary>
		public List<Comment> Comments
		{
			get { return _Comments; }
		}


		/// <summary>
		/// Gets and sets the tags as a string
		/// </summary>
		public string TagString
		{
			get
			{
				string[] tags = new string[Tags.Count];

				for (int i = 0; i < Tags.Count; i++)
					tags[i] = Tags[i];

				return string.Join(",", tags);
			}
			set
			{
				Tags.Clear();
				if (value.Trim().Length > 0)
				{
					string[] alltags = value.Split(',');
					foreach (string tag in alltags)
						Tags.Add(tag.Trim().ToLowerInvariant());
				}
			}
		}

		#endregion

		#region methods

		/// <summary>
		/// Provides the formatted string for the relative link.  Override to use a format other than:
		///     [relative URL]/[Prefix e.g. Post]/[Slug]/[FileExtension]
		/// 
		/// Example:
		///      dotnetblogengine.com/Post/This-Is-My-Post.aspx
		/// </summary>
		/// <returns>string</returns>
		protected virtual string RelativeLinkCore()
		{
			return Utils.RelativeWebRoot + RelativeLinkPrefix() + Utils.RemoveIllegalCharacters(Slug) + BlogSettings.Instance.FileExtension;
		}
        /// <summary>
        /// Provides the formatted string for the absolute link.  Override to use a format other than the 
        /// default relative link.
        /// </summary>
        /// <returns></returns>
        protected virtual string AbsoluteLinkCore()
        {
            return RelativeLink;
        }
		/// <summary>
		/// returns the relative link with prefixing page number
		/// </summary>
		/// <returns></returns>
		public virtual string RelativeLinkWithPage(int page)
		{
			return Utils.RelativeWebRoot + RelativeLinkPrefix() + page.ToString() + "/" + Utils.RemoveIllegalCharacters(Slug) + BlogSettings.Instance.FileExtension;
		}

		/// <summary>
		/// Required implementation for most derived classes as most classes will use the default relative link format
		/// </summary>
		/// <returns></returns>
		protected abstract string RelativeLinkPrefix();

		/// <summary>
		/// Required implementation for derived classes to return the aspx page name that will handle the permalink name.
		/// </summary>
		/// <returns></returns>
		protected abstract string PermaLinkPrefix();

		/// <summary>
		/// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
		/// </returns>
		public override string ToString()
		{
			return Title;
		}

		/// <summary>
		/// Checks to see if the specified title has already been used
		/// by another item of T./>.
		/// <remarks>
		/// Titles must be unique because the title can be used to form part of the URL.
		/// Titles can be non-unique across different Ts (e.g. Posts, Pages)
		/// </remarks>
		/// </summary>
		public static bool IsTitleUnique(string title)
		{
			foreach (T item in PublishableBase<T>.Items)
			{
				if (Utils.RemoveIllegalCharacters(item.Title).Equals(Utils.RemoveIllegalCharacters(title), StringComparison.OrdinalIgnoreCase))
				{
					return false;
				}
			}

			return true;
		}

		/// <summary>
		/// Returns all visible/published items in the specified category only
		/// </summary>
		public static List<T> GetByCategory(Guid categoryId)
		{
			Category cat = Category.GetById(categoryId);
			return GetByCategory(cat, true, false);
		}

        /// <summary>
        /// Returns all visible/published items in the specified category and optionally all sub-categories.
        /// </summary>
        public static List<T> GetByCategory(Guid categoryId, bool includeSubCategories)
        {
            Category cat = Category.GetById(categoryId);
            return GetByCategory(cat, true, includeSubCategories);
        }

        /// <summary>
        /// Returns items in the specified category. Options:
        ///  - only return visible items
        ///  - include sub categories   
        /// </summary>
        /// <remarks>
        /// visibleOnly should be set to true to only return viewable items
        /// includeSubCategories should be set to true to include any child/sub categories
        /// </remarks>
        public static List<T> GetByCategory(Guid categoryId, bool visibleOnly, bool includeSubCategories)
        {
            Category cat = Category.GetById(categoryId);
            return GetByCategory(cat, visibleOnly, includeSubCategories);
        }

		/// <summary>
		/// Returns all visible/published items in the specified category only
		/// </summary>
		public static List<T> GetByCategory(Category category)
		{
            return GetByCategory(category, true, false);
        }

        /// <summary>
        /// Returns all visible/published items in the specified category and optionally all sub-categories
        /// </summary>
        public static List<T> GetByCategory(Category category, bool includeSubCategories)
        {
            return GetByCategory(category, true, includeSubCategories);
        }

        /// <summary>
        /// Returns items in the specified category. Options:
        ///  - only return visible items
        ///  - include sub categories   
        /// </summary>
        /// <remarks>
        /// visibleOnly should be set to true to only return viewable items
        /// includeSubCategories should be set to true to include any child/sub categories
        /// </remarks>
        public static List<T> GetByCategory(Category category, bool visibleOnly, bool includeSubCategories)
        {
            if (category == null)
            {
                return new List<T>();
            }

            //only want this category 
            if (!includeSubCategories)
            {
                return GetCategoryItems(category, visibleOnly);
            }

            //wants multiple categories - get a list of categories and fill a single list.
            List<T> list = new List<T>();

            List<Category> catList = Category.GetChildCategories(category);
            catList.Add(category);

            foreach (Category cat in catList)
            {
                list.AddRange(GetCategoryItems(cat, visibleOnly));
            }
            list.Sort();

            return list;
        }

        /// <summary>
        /// Returns items in the specified category and optionally whether they are visible.  
        /// </summary>
        private static List<T> GetCategoryItems(Category category, bool visibleOnly)
        {
            List<T> list = BlogBase<T>.Items.FindAll(delegate(T item)
            {
                return ((!visibleOnly || (visibleOnly && item.IsVisible)) && (item.Categories.Contains(category)));
            });

			list.Sort();
			return list;
		}

        /// <summary>
		/// Returns all items tagged with the specified tag.
		/// </summary>
		public static List<T> GetByTag(string tag)
		{
            //TODO: remove commented out code when tested
            //List<T> list = new List<T>();
            //foreach (T item in BlogBase<T>.Items)
            //{
            //    if (item.Tags.Contains(tag))
            //    {
            //        list.Add(item);
            //    }
            //}

            //return list;

            List<T> list = BlogBase<T>.Items.FindAll(delegate(T item)
            {
                return item.Tags.Contains(tag);
            });

            return list;
		}
		/// <summary>
		/// Returns all items written by the specified author.
		/// </summary>
		public static List<T> GetByAuthor(string author)
		{
            //TODO: remove commented out code when tested.
            //List<T> list = new List<T>();
            //foreach (T item in BlogBase<T>.Items)
            //{
            //    string legalTitle = Utils.RemoveIllegalCharacters(item.Author);
            //    if (Utils.RemoveIllegalCharacters(author).Equals(legalTitle, StringComparison.OrdinalIgnoreCase))
            //    {
            //        list.Add(item);
            //    }
            //}

            //return list;

            List<T> list = BlogBase<T>.Items.FindAll(delegate(T item)
            {
                string legalTitle = Utils.RemoveIllegalCharacters(item.Author);
                return Utils.RemoveIllegalCharacters(author).Equals(legalTitle, StringComparison.OrdinalIgnoreCase);
            });

            return list;
		}

		/// <summary>
		/// Removes a comment from the collection and saves the item.
		/// </summary>
		/// <param name="comment">The comment to remove from the item.</param>
		public void RemoveComment(Comment comment)
		{
            CancelEventArgs args = new CancelEventArgs();
			OnCommentRemoving(comment, args);

            if (!args.Cancel)
            {
                Comments.Remove(comment);
                DataUpdate();
                OnCommentRemoved(comment);
                comment = null;
            }
		}

		/// <summary>
		/// Approves a Comment for item.
		/// </summary>
		/// <param name="comment">The Comment to approve</param>
		public void ApproveComment(Comment comment)
		{
            CancelEventArgs args = new CancelEventArgs();
			Comment.OnApproving(comment, args);

            if (!args.Cancel)
            {
                comment.IsApproved = true;
                DataUpdate();
                Comment.OnApproved(comment);
            }
		}

		/// <summary>
		/// Approves all the comments in an item.  Included to save time on the approval process.
		/// </summary>
		public void ApproveAllComments()
		{
			foreach (Comment comment in Comments)
			{
				ApproveComment(comment);
			}
		}

		/// <summary>
		/// Validates the item instance.
		/// </summary>
		/// <remarks>
		/// Each derived class should override this and ensure they call base.ValidationRules()
		/// to get a composite set of rules.
		///</remarks>
		protected override void ValidationRules()
		{
			base.ValidationRules();
		}

		/// <summary>
		/// Adds a rating to the item.
		/// </summary>
		public void Rate(int rating)
		{
			if (Raters > 0)
			{
				float total = Raters * Rating;
				total += rating;
				Raters++;
				Rating = (float)(total / Raters);
			}
			else
			{
				Raters = 1;
				Rating = rating;
			}

			DataUpdate();

			OnRated(this);
		}

		/// <summary>
		/// Adds a comment to the collection and saves the item.
		/// </summary>
		/// <param name="comment">The comment to add to the item.</param>
		public void AddComment(Comment comment)
		{
            CancelEventArgs args = new CancelEventArgs();
            OnCommentAdding(comment, args);
            if (!args.Cancel)
            {
                Comments.Add(comment);
                DataUpdate();
                OnCommentAdded(comment);
                SendNotifications(comment);
            }
		}

		/// <summary>
		/// Imports a comment to comment collection and saves.  Does not
		/// notify user or run extension events.
		/// </summary>
		/// <param name="comment">The comment to add to the post.</param>
		public void ImportComment(Comment comment)
		{
			Comments.Add(comment);
			DataUpdate();
		}

		/// <summary>
		/// Sends a notification to all visitors  that has registered
		/// to retrieve notifications for the specific post.
		/// </summary>
		private void SendNotifications(Comment comment)
		{
			if (NotificationEmails.Count == 0)
			{
				return;
			}

			MailMessage mail = new MailMessage();
			mail.From = new MailAddress(BlogSettings.Instance.Email, BlogSettings.Instance.Name);
			mail.Subject = "New comment on " + Title;
			mail.Body = "Comment by " + comment.Author + Environment.NewLine + Environment.NewLine;
			mail.Body += comment.Content + Environment.NewLine + Environment.NewLine + AbsoluteLink.ToString();

			foreach (string email in NotificationEmails)
			{
				if (email != comment.Email)
				{
					mail.To.Clear();                        //TODO - remove mail.to.clear? send to all addresses in one go?  possibly should be BCC?
					mail.To.Add(email);
					Utils.SendMailMessageAsync(mail);
				}
			}
		}

		/// <summary>
		/// Sets the tags given a string of tags
		/// </summary>
		public void SetTags(string tags)
		{
			Tags.Clear();
			if (tags.Trim().Length > 0)
			{
				string[] alltags = tags.Split(',');
				foreach (string tag in alltags)
					Tags.Add(tag.Trim().ToLowerInvariant());
			}
		}

		#endregion

		#region events

		/// <summary>
		/// Occurs when the item is being served to the output stream.
		/// </summary>
		public static event EventHandler<ServingEventArgs> Serving;
		/// <summary>
		/// Raises the event in a safe way
		/// </summary>
		public static void OnServing(T item, ServingEventArgs arg)
		{
			if (Serving != null)
			{
				Serving(item, arg);
			}
		}

		/// <summary>
		/// Raises the Serving event
		/// </summary>
		public void OnServing(ServingEventArgs eventArgs)
		{
			if (Serving != null)
			{
				Serving(this, eventArgs);
			}
		}

		/// <summary>
		/// Occurs when a visitor rates the item.
		/// </summary>
		public static event EventHandler<EventArgs> Rated;
		/// <summary>
		/// Raises the event in a safe way
		/// </summary>
		protected virtual void OnRated(PublishableBase<T> item)
		{
			if (Rated != null)
			{
				Rated(item, new EventArgs());
			}
		}

		/// <summary>
		/// Occurs before a new comment is added.
		/// </summary>
		public static event EventHandler<CancelEventArgs> CommentAdding;
		/// <summary>
		/// Raises the event in a safe way
		/// </summary>
		protected virtual void OnCommentAdding(Comment comment, CancelEventArgs args)
		{
			if (CommentAdding != null)
			{
				CommentAdding(comment, args);
			}
		}

		/// <summary>
		/// Occurs when a comment is added.
		/// </summary>
		public static event EventHandler<EventArgs> CommentAdded;
		/// <summary>
		/// Raises the event in a safe way
		/// </summary>
		protected virtual void OnCommentAdded(Comment comment)
		{
			if (CommentAdded != null)
			{
				CommentAdded(comment, new EventArgs());
			}
		}

		/// <summary>
		/// Occurs before comment is removed.
		/// </summary>
		public static event EventHandler<CancelEventArgs> CommentRemoving;
		/// <summary>
		/// Raises the event in a safe way
		/// </summary>
		protected virtual void OnCommentRemoving(Comment comment, CancelEventArgs args)
		{
			if (CommentRemoving != null)
			{
				CommentRemoving(comment, args);
			}
		}

		/// <summary>
		/// Occurs when a comment has been removed.
		/// </summary>
		public static event EventHandler<EventArgs> CommentRemoved;
		/// <summary>
		/// Raises the event in a safe way
		/// </summary>
		protected virtual void OnCommentRemoved(Comment comment)
		{
			if (CommentRemoved != null)
			{
				CommentRemoved(comment, new EventArgs());
			}
		}

		#endregion
	}
}
